package com.gitee.wsl.time

import com.gitee.wsl.ext.base.today
import com.gitee.wsl.ext.find
import com.gitee.wsl.ext.group
import com.gitee.wsl.ext.matcher
import com.gitee.wsl.result.runCatching
import kotlinx.datetime.*
import kotlinx.datetime.format.DateTimeComponents
import kotlinx.datetime.format.DateTimeFormat
import kotlinx.datetime.format.DateTimeFormatBuilder
import kotlinx.datetime.format.parse
import kotlinx.datetime.minus
import kotlinx.datetime.plus
import kotlin.time.Instant
import kotlin.math.PI
import kotlin.math.sin
import kotlin.time.Duration.Companion.days
import kotlin.time.ExperimentalTime


object T {

    internal const val DAYS_IN_COMMON_YEAR = 365L
    internal const val DAYS_PER_400_YEAR_CYCLE = 146_097
    internal const val NUMBER_OF_400_YEAR_CYCLES_FROM_0000_TO_1970 = 5L
    internal const val LEAP_YEARS_FROM_1970_TO_2000 = 7L
    internal const val YEARS_FROM__1970_TO_2000 = 30L

    internal const val DAYS_FROM_0000_TO_1970 =
        (DAYS_PER_400_YEAR_CYCLE * NUMBER_OF_400_YEAR_CYCLES_FROM_0000_TO_1970) -
                (YEARS_FROM__1970_TO_2000 * DAYS_IN_COMMON_YEAR + LEAP_YEARS_FROM_1970_TO_2000)


    @PublishedApi
    internal const val NANOSECONDS_PER_MICROSECOND: Int = 1000

    @PublishedApi
    internal const val NANOSECONDS_PER_MILLISECOND: Int = 1000000

    @PublishedApi
    internal const val NANOSECONDS_PER_SECOND: Int = 1000000000

    @PublishedApi
    internal const val NANOSECONDS_PER_MINUTE: Long = 60000000000L

    @PublishedApi
    internal const val NANOSECONDS_PER_HOUR: Long = 3600000000000L

    @PublishedApi
    internal const val NANOSECONDS_PER_DAY: Long = 86400000000000L

    @PublishedApi
    internal const val MICROSECONDS_PER_MILLISECOND: Int = 1000

    @PublishedApi
    internal const val MICROSECONDS_PER_SECOND: Int = 1000000

    @PublishedApi
    internal const val MICROSECONDS_PER_MINUTE: Int = 60000000

    @PublishedApi
    internal const val MICROSECONDS_PER_HOUR: Long = 3600000000L

    @PublishedApi
    internal const val MICROSECONDS_PER_DAY: Long = 86400000000L

    @PublishedApi
    internal const val MILLISECONDS_PER_SECOND: Int = 1000

    @PublishedApi
    internal const val MILLISECONDS_PER_MINUTE: Int = 60000

    @PublishedApi
    internal const val MILLISECONDS_PER_HOUR: Int = 3600000

    @PublishedApi
    internal const val MILLISECONDS_PER_DAY: Int = 86400000

    @PublishedApi
    internal const val SECONDS_PER_MINUTE: Int = 60

    @PublishedApi
    internal const val SECONDS_PER_HOUR: Int = 3600

    @PublishedApi
    internal const val SECONDS_PER_DAY: Int = 86400

    @PublishedApi
    internal const val MINUTES_PER_HOUR: Int = 60

    @PublishedApi
    internal const val MINUTES_PER_DAY: Int = 1440

    @PublishedApi
    internal const val HOURS_PER_DAY: Int = 24

    @PublishedApi
    internal const val DAYS_PER_WEEK: Int = 7

    @PublishedApi
    internal const val MONTHS_PER_YEAR: Int = 12

    @PublishedApi
    internal const val MONTHS_PER_DECADE: Int = 120

    @PublishedApi
    internal const val MONTHS_PER_CENTURY: Int = 1200

    @PublishedApi
    internal const val YEARS_PER_DECADE: Int = 10

    @PublishedApi
    internal const val YEARS_PER_CENTURY: Int = 100

    @PublishedApi
    internal const val DECADES_PER_CENTURY: Int = 10


    const val SECOND_IN_MILLIS: Long = 1000
    const val MINUTE_IN_MILLIS: Long = SECOND_IN_MILLIS * 60
    const val HOUR_IN_MILLIS: Long = MINUTE_IN_MILLIS * 60
    const val DAY_IN_MILLIS: Long = HOUR_IN_MILLIS * 24
    const val WEEK_IN_MILLIS: Long = DAY_IN_MILLIS * 7

    val Formats get() = DateTimeComponents.Formats

    const val MS_TO_NS = 1_000_000L
    const val MAX_MS = Long.MAX_VALUE / MS_TO_NS


    const val PHYSICAL_PERIOD = 23
    const val EMOTIONAL_PERIOD = 28
    const val INTELLECTUAL_PERIOD = 33

    val defalutFormat = listOf(
        Formats.ISO_DATE_TIME_OFFSET,
        Formats.RFC_1123,
        Formats.customFormat
    )

    /**
     * First-line overflow protection -- limit maximal delay.
     * Delays longer than this one (~146 years) are considered to be delayed "forever".
     */
    private const val MAX_DELAY_NS = Long.MAX_VALUE / 2

    fun delayToNanos(timeMillis: Long): Long = when {
        timeMillis <= 0 -> 0L
        timeMillis >= MAX_MS -> Long.MAX_VALUE
        else -> timeMillis * MS_TO_NS
    }

    fun delayNanosToMillis(timeNanos: Long): Long = timeNanos / MS_TO_NS

    fun isLeapYear(year: Int): Boolean {
        val prolepticYear: Long = year.toLong()
        return prolepticYear and 3 == 0L && (prolepticYear % 100 != 0L || prolepticYear % 400 == 0L)
    }

    fun createFormat(block: DateTimeFormatBuilder.WithDateTimeComponents.() -> Unit): DateTimeFormat<DateTimeComponents> =
        DateTimeComponents.Format(block)

    fun parse(
        input: CharSequence,
        format: DateTimeFormat<DateTimeComponents>,
    ): DateTimeComponents = DateTimeComponents.parse(input, format)

    fun parse(
        input: CharSequence,
        formats: List<DateTimeFormat<DateTimeComponents>> = defalutFormat,
    ): DateTimeComponents {
        formats.forEach {
            try {
                return parse(input, it)
            } catch (_: IllegalArgumentException) {
            }
        }
        throw IllegalArgumentException()
    }

    fun parseDate(
        input: CharSequence,
        formats: List<DateTimeFormat<DateTimeComponents>> = defalutFormat,
    ) = runCatching {
        parse(input, formats).toLocalDate()
    }

    fun parseDateExt(input: CharSequence) = runCatching {
        DateTimeFormatHelper.INSTANCE.getDateMillisFromString(input.toString())?.toLocalDate()
    }

    fun parseDateTimeExt(input: CharSequence) = runCatching {
        DateTimeFormatHelper.INSTANCE.getDateMillisFromString(input.toString())?.toLocalDateTime()
    }

    fun parseDateTime(
        input: CharSequence,
        formats: List<DateTimeFormat<DateTimeComponents>> = defalutFormat,
    ) = runCatching {
        parse(input, formats).toLocalDateTime()
    }

    fun parseTime(
        input: CharSequence,
        formats: List<DateTimeFormat<DateTimeComponents>> = defalutFormat,
    ) = runCatching {
        parse(input, formats).toLocalTime()
    }

    fun workdaysCount(
        startDate: LocalDateTime,
        days: Int,
        holidays: List<LocalDateTime>,
        weekendPolicy: WeekendPolicy,
    ): Int {
        return workdaysCount(startDate, startDate - days.days, holidays, weekendPolicy)
    }

    fun workdaysCount(
        startDate: LocalDateTime,
        endDate: LocalDateTime,
        holidays: List<LocalDateTime>,
        weekendPolicy: WeekendPolicy,
    ): Int {
        val holidays = holidaysInRange(startDate, endDate, holidays.sorted())
        val daysToWeekend: Int = WeekendPolicy.daysToWeekend(weekendPolicy, startDate)
        val daysFromWeekend: Int = WeekendPolicy.daysFromWeekend(weekendPolicy, endDate)
        val daysBetween = (startDate-endDate).inWholeDays.toInt()

        //2+ [xxx+2]/7*5
        var tmpWeekdays = (daysBetween - daysToWeekend - daysFromWeekend - 2) / 7 * 5
        tmpWeekdays -= holidays.size
        return tmpWeekdays + daysToWeekend + daysFromWeekend
    }

    private fun holidaysInRange(
        startDate: LocalDateTime,
        endDate: LocalDateTime,
        holidays: List<LocalDateTime>,
    ): List<LocalDateTime> {
        if (holidays.isEmpty()) {
            return holidays
        }
        val idxstart = findIdx(0, holidays.size - 1, startDate, holidays)
        val idxend = findIdx(0, holidays.size - 1, endDate, holidays)
        return holidays.subList(idxstart, idxend + 1)
    }

    private fun findIdx(
        startidx: Int,
        endidx: Int,
        endDate: LocalDateTime,
        holidays: List<LocalDateTime>,
    ): Int {
        if (startidx == endidx) {
            return startidx
        }
        var pivot = (endidx - startidx) / 2
        if (holidays[pivot] == endDate) {
            return pivot
        }
        return if (holidays[pivot] < endDate) {
            findIdx(++pivot, endidx, endDate, holidays)
        } else {
            findIdx(startidx, pivot, endDate, holidays)
        }
    }

    fun calculateCandidateDays(year: Int, monthValue: Int,localeStartDayOfWeek:Int = DayOfWeek.SUNDAY.isoDayNumber): List<LocalDate> {
        val localDate = LocalDate(year, monthValue + 1, 1)
        val startDayOfWeek = localDate.dayOfWeek.isoDayNumber + 1
//        val startDayOfWeek = calendar.get(Calendar.DAY_OF_WEEK) // Start at Sunday(1)

        // Start day at the first `localeStartDayOfWeek(e.g. Sunday)` before this month
        // If localeStartDOW = Sunday(1):
        //   Wednesday(4) - Sunday(1) = 3
        // If localeStartDOW = Monday(2):
        //   Sunday(1) - Monday(2) = -1 -> 6   (case 1)
        //   Wednesday(4) - Monday(2) = 2
        // If this month (5/1) is Wednesday(4th), the offset should be 3, the start day should be 5/1 - 3days = 4/28
        // If this month (9/1) is Sunday(1th), the offset should be 0, the start day should be 7/1 - 0days = 7/1

        var startDayOffset = startDayOfWeek - localeStartDayOfWeek
        if (startDayOffset < 0) { // for case 1
            startDayOffset += 7
        }

        val startDay = localDate.minus(DatePeriod(days = startDayOffset))

        // e.g from 4-29 to 6-9
        val daysToDisplay = 7 * 6
        return List(daysToDisplay) { i ->
            startDay.plus(DatePeriod(days = i))
        }
    }

    @OptIn(ExperimentalTime::class)
    fun calculateCandidateYears(currentYear: Int): List<Year> {
        val startYear = currentYear / 10 * 10 // 2024 -> 2020
        val endYear = startYear + 16
        val candidateYears = (startYear until endYear).map {
            Year(it)
        }
        return candidateYears
    }


    fun calculateBiorhythmsForMonth(birthDate: LocalDate = LocalDate(2000,10,28), targetDate: LocalDate): List<BiorhythmState> {
        val startOfMonth = LocalDate(targetDate.year, targetDate.month, 1)
        val lastDayOfMonth = startOfMonth.plus(1, DateTimeUnit.MONTH).minus(1, DateTimeUnit.DAY)

        return (startOfMonth.dayOfMonth..lastDayOfMonth.dayOfMonth).map { day ->
            val targetDay = LocalDate(targetDate.year, targetDate.month, day)
            calculateBiorhythms(birthDate, targetDay)
        }
    }

    fun calculateBiorhythmsForWeek(birthDate: LocalDate = LocalDate(2000,5,23), targetDate: LocalDate): List<BiorhythmState> {
        return (-7..7).map { day ->
            val targetDay = targetDate.plus(DatePeriod(days = day))
            calculateBiorhythms(birthDate, targetDay)
        }
    }

    fun calculateBiorhythms(birthDate: LocalDate, targetDate: LocalDate): BiorhythmState {
        // 计算出生日至目标日期的总天数
        val totalDays = birthDate.daysUntil(targetDate)
        // 计算每个周期的余数
        val physical = BiorhythmRatio(totalDays, PHYSICAL_PERIOD)
        val emotional = BiorhythmRatio(totalDays, EMOTIONAL_PERIOD)
        val intellectual = BiorhythmRatio(totalDays, INTELLECTUAL_PERIOD)
        return BiorhythmState(targetDate, physical, emotional, intellectual)
    }

    fun BiorhythmRatio(totalDays: Int, period: Int): Int {
        val biorhythmValue = sin((2 * PI * totalDays) / period)
        return (biorhythmValue * 100).toInt()
    }


    /**
     * Converts from the Microsoft [filetime] format to the Java epoch millis format.
     *
     *  * Filetime's unit is 100 nanoseconds, and 0 is 1601-01-01T00:00:00Z.
     *  * Java epoch millis' unit is 1 millisecond, and 0 is 1970-01-01T00:00:00Z.
     *
     * See also https://learn.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-filetime
     */
    internal fun filetimeToEpochMillis(filetime: Long): Long {
        // There's 11,644,473,600,000 milliseconds between 1601-01-01T00:00:00Z and 1970-01-01T00:00:00Z.
        //   val years = 1_970 − 1_601
        //   val leapYears = floor(years / 4) − floor(years / 100)
        //   val days = (years * 365) + leapYears
        //   val millis = days * 24 * 60 * 60 * 1_000
        return filetime / 10_000 - 11_644_473_600_000L
    }

    /**
     * Converts a 32-bit DOS date+time to milliseconds since epoch. Note that this function interprets
     * a value with no time zone as a value with the local time zone.
     */
    @OptIn(ExperimentalTime::class)
    fun dosDateTimeToEpochMillis(date: Int, time: Int): Long? {
        if (time == -1) {
            return null
        }
        return LocalDateTime(
            year = 1980 + (date shr 9 and 0x7f),
            month = date shr 5 and 0xf,
            day = date and 0x1f,
            hour =  time shr 11 and 0x1f,
            minute = time shr 5 and 0x3f,
            second = time and 0x1f shl 1,
        ).toInstant(TimeZone.UTC).toEpochMilliseconds()
    }

    fun age(birthDate: LocalDate, targetDate: LocalDate) = (targetDate - birthDate).years

    fun age(birthDate: LocalDateTime, targetDate: LocalDateTime) = age(birthDate.date,targetDate.date)

    fun thisYear() = today.year

    /**
     * 验证是否为日期
     *
     * @param year  年，从1900年开始计算
     * @param month 月，从1开始计数
     * @param day   日，从1开始计数
     * @return 是否为日期
     */
    fun isBirthday(year: Int, month: Int, day: Int): Boolean {
        // 验证年
        val thisYear: Int = thisYear()
        if (year !in 1900..thisYear) {
            return false
        }

        // 验证月
        if (month !in 1..12) {
            return false
        }

        // 验证日
        if (day !in 1..31) {
            return false
        }
        // 检查几个特殊月的最大天数
        if (day == 31 && (month == 4 || month == 6 || month == 9 || month == 11)) {
            return false
        }
        if (month == 2) {
            // 在2月，非闰年最大28，闰年最大29
            return day < 29 || (day == 29 && isLeapYear(year))
        }
        return true
    }

    /**
     * 验证是否为生日<br></br>
     * 只支持以下几种格式：
     *
     *  * yyyyMMdd
     *  * yyyy-MM-dd
     *  * yyyy/MM/dd
     *  * yyyy.MM.dd
     *  * yyyy年MM月dd日
     *
     *
     * @param value 值
     * @return 是否为生日
     */
    fun isBirthday(value: CharSequence): Boolean {
        val matcher = BIRTHDAY.toRegex().matcher(value.toString())
        if (matcher.find()) {
            val year = matcher!!.group(1)!!.toInt()
            val month = matcher.group(3)!!.toInt()
            val day = matcher.group(5)!!.toInt()
            return isBirthday(year, month, day)
        }
        return false
    }

    /**
     * 生日
     */
    private val BIRTHDAY: String = "^(\\d{2,4})([/\\-.年]?)(\\d{1,2})([/\\-.月]?)(\\d{1,2})日?$"
}

data class BiorhythmState(
    val localDate: LocalDate,
    val physical: Int,
    val emotional: Int,
    val intellectual: Int,
)

